1 import { initializer } from './initializer.js';
2 import { htmlParser } from './html-parser.js';
3
4 /**
5 * Welcome to Typed.js!
6 * @param {string} elementId HTML element ID _OR_ HTML element
7 * @param {object} options options object
8 * @returns {object} a new Typed object
9 */
10 export default class Typed {
11 constructor(elementId, options) {
12 // Initialize it up
13 initializer.load(this, options, elementId);
14 // All systems go!
15 this.begin();
16 }
17
18 /**
19 * Toggle start() and stop() of the Typed instance
20 * @public
21 */
22 toggle() {
23 this.pause.status ? this.start() : this.stop();
24 }
25
26 /**
27 * Stop typing / backspacing and enable cursor blinking
28 * @public
29 */
30 stop() {
31 if (this.typingComplete) return;
32 if (this.pause.status) return;
33 this.toggleBlinking(true);
34 this.pause.status = true;
35 this.options.onStop(this.arrayPos, this);
36 }
37
38 /**
39 * Start typing / backspacing after being stopped
40 * @public
41 */
42 start() {
43 if (this.typingComplete) return;
44 if (!this.pause.status) return;
45 this.pause.status = false;
46 if (this.pause.typewrite) {
47 this.typewrite(this.pause.curString, this.pause.curStrPos);
48 } else {
49 this.backspace(this.pause.curString, this.pause.curStrPos);
50 }
51 this.options.onStart(this.arrayPos, this);
52 }
53
54 /**
55 * Destroy this instance of Typed
56 * @public
57 */
58 destroy() {
59 this.reset(false);
60 this.options.onDestroy(this);
61 }
62
63 /**
64 * Reset Typed and optionally restarts
65 * @param {boolean} restart
66 * @public
67 */
68 reset(restart = true) {
69 clearInterval(this.timeout);
70 this.replaceText('');
71 if (this.cursor && this.cursor.parentNode) {
72 this.cursor.parentNode.removeChild(this.cursor);
73 this.cursor = null;
74 }
75 this.strPos = 0;
76 this.arrayPos = 0;
77 this.curLoop = 0;
78 if (restart) {
79 this.insertCursor();
80 this.options.onReset(this);
81 this.begin();
82 }
83 }
84
85 /**
86 * Begins the typing animation
87 * @private
88 */
89 begin() {
90 this.typingComplete = false;
91 this.shuffleStringsIfNeeded(this);
92 this.insertCursor();
93 if (this.bindInputFocusEvents) this.bindFocusEvents();
94 this.timeout = setTimeout(() => {
95 // Check if there is some text in the element, if yes start by backspacing the default message
96 if (!this.currentElContent || this.currentElContent.length === 0) {
97 this.typewrite(this.strings[this.sequence[this.arrayPos]], this.strPos);
98 } else {
99 // Start typing
100 this.backspace(this.currentElContent, this.currentElContent.length);
101 }
102 }, this.startDelay);
103 }
104
105 /**
106 * Called for each character typed
107 * @param {string} curString the current string in the strings array
108 * @param {number} curStrPos the current position in the curString
109 * @private
110 */
111 typewrite(curString, curStrPos) {
112 if (this.fadeOut && this.el.classList.contains(this.fadeOutClass)) {
113 this.el.classList.remove(this.fadeOutClass);
114 if (this.cursor) this.cursor.classList.remove(this.fadeOutClass);
115 }
116
117 const humanize = this.humanizer(this.typeSpeed);
118 let numChars = 1;
119
120 if (this.pause.status === true) {
121 this.setPauseStatus(curString, curStrPos, true);
122 return;
123 }
124
125 // contain typing function in a timeout humanize'd delay
126 this.timeout = setTimeout(() => {
127 // skip over any HTML chars
128 curStrPos = htmlParser.typeHtmlChars(curString, curStrPos, this);
129
130 let pauseTime = 0;
131 let substr = curString.substr(curStrPos);
132 // check for an escape character before a pause value
133 // format: \^\d+ .. eg: ^1000 .. should be able to print the ^ too using ^^
134 // single ^ are removed from string
135 if (substr.charAt(0) === '^') {
136 if (/^\^\d+/.test(substr)) {
137 let skip = 1; // skip at least 1
138 substr = /\d+/.exec(substr)[0];
139 skip += substr.length;
140 pauseTime = parseInt(substr);
141 this.temporaryPause = true;
142 this.options.onTypingPaused(this.arrayPos, this);
143 // strip out the escape character and pause value so they're not printed
144 curString = curString.substring(0, curStrPos) + curString.substring(curStrPos + skip);
145 this.toggleBlinking(true);
146 }
147 }
148
149 // check for skip characters formatted as
150 // "this is a `string to print NOW` ..."
151 if (substr.charAt(0) === '`') {
152 while (curString.substr(curStrPos + numChars).charAt(0) !== '`') {
153 numChars++;
154 if (curStrPos + numChars > curString.length) break;
155 }
156 // strip out the escape characters and append all the string in between
157 const stringBeforeSkip = curString.substring(0, curStrPos);
158 const stringSkipped = curString.substring(stringBeforeSkip.length + 1, curStrPos + numChars);
159 const stringAfterSkip = curString.substring(curStrPos + numChars + 1);
160 curString = stringBeforeSkip + stringSkipped + stringAfterSkip;
161 numChars--;
162 }
163
164 // timeout for any pause after a character
165 this.timeout = setTimeout(() => {
166 // Accounts for blinking while paused
167 this.toggleBlinking(false);
168
169 // We're done with this sentence!
170 if (curStrPos === curString.length) {
171 this.doneTyping(curString, curStrPos);
172 } else {
173 this.keepTyping(curString, curStrPos, numChars);
174 }
175 // end of character pause
176 if (this.temporaryPause) {
177 this.temporaryPause = false;
178 this.options.onTypingResumed(this.arrayPos, this);
179 }
180 }, pauseTime);
181
182 // humanized value for typing
183 }, humanize);
184 }
185
186 /**
187 * Continue to the next string & begin typing
188 * @param {string} curString the current string in the strings array
189 * @param {number} curStrPos the current position in the curString
190 * @private
191 */
192 keepTyping(curString, curStrPos, numChars) {
193 // call before functions if applicable
194 if (curStrPos === 0) {
195 this.toggleBlinking(false);
196 this.options.preStringTyped(this.arrayPos, this);
197 }
198 // start typing each new char into existing string
199 // curString: arg, this.el.html: original text inside element
200 curStrPos += numChars;
201 const nextString = curString.substr(0, curStrPos);
202 this.replaceText(nextString);
203 // loop the function
204 this.typewrite(curString, curStrPos);
205 }
206
207 /**
208 * We're done typing all strings
209 * @param {string} curString the current string in the strings array
210 * @param {number} curStrPos the current position in the curString
211 * @private
212 */
213 doneTyping(curString, curStrPos) {
214 // fires callback function
215 this.options.onStringTyped(this.arrayPos, this);
216 this.toggleBlinking(true);
217 // is this the final string
218 if (this.arrayPos === this.strings.length - 1) {
219 // callback that occurs on the last typed string
220 this.complete();
221 // quit if we wont loop back
222 if (this.loop === false || this.curLoop === this.loopCount) {
223 return;
224 }
225 }
226 this.timeout = setTimeout(() => {
227 this.backspace(curString, curStrPos);
228 }, this.backDelay);
229 }
230
231 /**
232 * Backspaces 1 character at a time
233 * @param {string} curString the current string in the strings array
234 * @param {number} curStrPos the current position in the curString
235 * @private
236 */
237 backspace(curString, curStrPos) {
238 if (this.pause.status === true) {
239 this.setPauseStatus(curString, curStrPos, true);
240 return;
241 }
242 if (this.fadeOut) return this.initFadeOut();
243
244 this.toggleBlinking(false);
245 const humanize = this.humanizer(this.backSpeed);
246
247 this.timeout = setTimeout(() => {
248 curStrPos = htmlParser.backSpaceHtmlChars(curString, curStrPos, this);
249 // replace text with base text + typed characters
250 const curStringAtPosition = curString.substr(0, curStrPos);
251 this.replaceText(curStringAtPosition);
252
253 // if smartBack is enabled
254 if (this.smartBackspace) {
255 // the remaining part of the current string is equal of the same part of the new string
256 let nextString = this.strings[this.arrayPos + 1];
257 if (nextString && curStringAtPosition === nextString.substr(0, curStrPos)) {
258 this.stopNum = curStrPos;
259 } else {
260 this.stopNum = 0;
261 }
262 }
263
264 // if the number (id of character in current string) is
265 // less than the stop number, keep going
266 if (curStrPos > this.stopNum) {
267 // subtract characters one by one
268 curStrPos--;
269 // loop the function
270 this.backspace(curString, curStrPos);
271 } else if (curStrPos <= this.stopNum) {
272 // if the stop number has been reached, increase
273 // array position to next string
274 this.arrayPos++;
275 // When looping, begin at the beginning after backspace complete
276 if (this.arrayPos === this.strings.length) {
277 this.arrayPos = 0;
278 this.options.onLastStringBackspaced();
279 this.shuffleStringsIfNeeded();
280 this.begin();
281 } else {
282 this.typewrite(this.strings[this.sequence[this.arrayPos]], curStrPos);
283 }
284 }
285 // humanized value for typing
286 }, humanize);
287 }
288
289 /**
290 * Full animation is complete
291 * @private
292 */
293 complete() {
294 this.options.onComplete(this);
295 if (this.loop) {
296 this.curLoop++;
297 } else {
298 this.typingComplete = true;
299 }
300 }
301
302 /**
303 * Has the typing been stopped
304 * @param {string} curString the current string in the strings array
305 * @param {number} curStrPos the current position in the curString
306 * @param {boolean} isTyping
307 * @private
308 */
309 setPauseStatus(curString, curStrPos, isTyping) {
310 this.pause.typewrite = isTyping;
311 this.pause.curString = curString;
312 this.pause.curStrPos = curStrPos;
313 }
314
315 /**
316 * Toggle the blinking cursor
317 * @param {boolean} isBlinking
318 * @private
319 */
320 toggleBlinking(isBlinking) {
321 if (!this.cursor) return;
322 // if in paused state, don't toggle blinking a 2nd time
323 if (this.pause.status) return;
324 if (this.cursorBlinking === isBlinking) return;
325 this.cursorBlinking = isBlinking;
326 const status = isBlinking ? 'infinite' : 0;
327 this.cursor.style.animationIterationCount = status;
328 }
329
330 /**
331 * Speed in MS to type
332 * @param {number} speed
333 * @private
334 */
335 humanizer(speed) {
336 return Math.round(Math.random() * speed / 2) + speed;
337 }
338
339 /**
340 * Shuffle the sequence of the strings array
341 * @private
342 */
343 shuffleStringsIfNeeded() {
344 if (!this.shuffle) return;
345 this.sequence = this.sequence.sort(() => Math.random() - 0.5);
346 }
347
348 /**
349 * Adds a CSS class to fade out current string
350 * @private
351 */
352 initFadeOut() {
353 this.el.className += ` ${this.fadeOutClass}`;
354 if (this.cursor) this.cursor.className += ` ${this.fadeOutClass}`;
355 return setTimeout(() => {
356 this.arrayPos++;
357 this.replaceText('');
358
359 // Resets current string if end of loop reached
360 if (this.strings.length > this.arrayPos) {
361 this.typewrite(this.strings[this.sequence[this.arrayPos]], 0);
362 } else {
363 this.typewrite(this.strings[0], 0);
364 this.arrayPos = 0;
365 }
366 }, this.fadeOutDelay);
367 }
368
369 /**
370 * Replaces current text in the HTML element
371 * depending on element type
372 * @param {string} str
373 * @private
374 */
375 replaceText(str) {
376 if (this.attr) {
377 this.el.setAttribute(this.attr, str);
378 } else {
379 if (this.isInput) {
380 this.el.value = str;
381 } else if (this.contentType === 'html') {
382 this.el.innerHTML = str;
383 } else {
384 this.el.textContent = str;
385 }
386 }
387 }
388
389 /**
390 * If using input elements, bind focus in order to
391 * start and stop the animation
392 * @private
393 */
394 bindFocusEvents() {
395 if (!this.isInput) return;
396 this.el.addEventListener('focus', (e) => {
397 this.stop();
398 });
399 this.el.addEventListener('blur', (e) => {
400 if (this.el.value && this.el.value.length !== 0) { return; }
401 this.start();
402 });
403 }
404
405 /**
406 * On init, insert the cursor element
407 * @private
408 */
409 insertCursor() {
410 if (!this.showCursor) return;
411 if (this.cursor) return;
412 this.cursor = document.createElement('span');
413 this.cursor.className = 'typed-cursor';
414 this.cursor.innerHTML = this.cursorChar;
415 this.el.parentNode && this.el.parentNode.insertBefore(this.cursor, this.el.nextSibling);
416 }
417 }